﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

using RayDen.Library.Core.Primitives;
using RayDen.Library.Data.Import.Loader3DS;
using RayDen.Library.Data.Import.OBJLoader;
using RayDen.Library.Entity.Frames;
using RayDen.Library.Entity.Interface;
using RayDen.Library.Entity.Scene;
using RayDen.RayEngine;
using RayDen.RayEngine.Core.Interface;
using RayDen.RayEngine.Data;
using RayDen.RayEngine.IntersectionEngines;
using OpenTK;
using OpenTK.Graphics.OpenGL;

using InfiniteLight = RayDen.RayEngine.Data.InfiniteLight;
using MeshLight = RayDen.RayEngine.Data.MeshLight;
using PointLight = RayDen.RayEngine.Data.PointLight;


namespace RayDen.GLPlayground
{

    public class GLScenePrimitive
    {
        public GeometryInfo geoData;
        public uint vaoHandle, positionVboHandle, normalVboHandle;
    }

    public class GLRenderer : IRenderConfig
    {

        public BeginMode PrimitiveRenderMode = BeginMode.Triangles;
        public RayEngineScene Scene;
        public ICamera Camera;
        private Dictionary<Type, Action<IFrameElement>> frameParsers;
        protected FrameDescription config;
        protected SceneGeometryInfo scene;
        protected MaterialInfo[] mats;
        private List<ILight> lights;
        private InfiniteLight envMap;

        private List<AABB> bboxes;

        private List<GLScenePrimitive> primitives;

        //GL State
        string vertexShaderSource = @"  #version 130

                                        precision highp float;

                                        uniform mat4 projection_matrix;
                                        uniform mat4 modelview_matrix;

                                        in vec3 in_position;


                                        void main(void)
                                        {
                                          gl_Position = projection_matrix * modelview_matrix * vec4(in_position, 1);
                                        }";

        string fragmentShaderSource = @"
                                        #version 130

                                        precision highp float;

                                        const vec3 ambient = vec3(0.1, 0.1, 0.1);
                                        const vec3 lightVecNormalized = normalize(vec3(0.5, 0.5, 2));
                                        const vec3 lightColor = vec3(0.9, 0.9, 0.7);

                                        out vec4 out_frag_color;

                                        void main(void)
                                        {
                                          float diffuse = 0.2;
                                          out_frag_color = vec4(ambient + diffuse * lightColor, 1.0);
                                        }";

        internal int vertexShaderHandle,
                    fragmentShaderHandle,
                    shaderProgramHandle,
                    modelviewMatrixLocation,
                    projectionMatrixLocation;
        internal Matrix4 projectionMatrix, modelviewMatrix;

        public GLRenderer()
        {
            frameParsers = new Dictionary<Type, Action<IFrameElement>>();
            frameParsers.Add(typeof(FrameCamera), cameraItem =>
            {
                var item = (FrameCamera)cameraItem;
                /*Camera = new PinholeCamera(width, height){ Fov = 40f};
                Camera.LookAt(item.Position, item.Target, item.Up);*/
                Camera = new BasicPerspectiveCamera((Point) item.Position, item.Target, -item.Up, Width, Height) { Fov = 40 };
            });

            frameParsers.Add(typeof(FrameObjFileReference), item =>
            {
                var objFileRef = (FrameObjFileReference)item;
                var loader = new GlobalIndexObjLoader();
                scene = loader.Load(objFileRef.ObjFilePath);
                var mload = new MaterialLoader();
                mats = mload.LoadMaterials(objFileRef.MtlFilePath);

                foreach (var materialInfo in mats)
                {
                    if (materialInfo.Name.ToLower().Contains("glass") ||
                         materialInfo.Name.ToLower().Contains("wire_134006006")
                         )
                    {
                        materialInfo.Kt = materialInfo.DiffuseReflectance;
                        materialInfo.DiffuseReflectance = RgbSpectrum.ZeroSpectrum();

                        //materialInfo.MediumInfo = Glass;
                    }
                    if (materialInfo.Name.ToLower().Contains("metal"))
                    {
                        materialInfo.SpecularReflectance = materialInfo.DiffuseReflectance;
                    }
                }
            });


            frameParsers.Add(typeof(Frame3DsFileReference), item =>
            {
                var sceneFileRef = (Frame3DsFileReference)item;
                var loader = new SceneLoader();
                scene = loader.Load(sceneFileRef.FilePath);
                mats = loader.LoadMaterials(sceneFileRef.FilePath);

                foreach (var materialInfo in mats)
                {
                    if (materialInfo.Name.ToLower().Contains("glass") ||
                         materialInfo.Name.ToLower().Contains("wire_134006006")
                         )
                    {
                        materialInfo.Kt = materialInfo.DiffuseReflectance;
                        materialInfo.DiffuseReflectance = RgbSpectrum.ZeroSpectrum();

                        //materialInfo.MediumInfo = Glass;
                    }
                    if (materialInfo.Name.ToLower().Contains("metal"))
                    {
                        materialInfo.SpecularReflectance = materialInfo.DiffuseReflectance;
                    }
                }
                if (scene.Cameras != null && scene.Cameras.Any())
                {
                    Camera = new BasicPerspectiveCamera((Point) scene.Cameras[0].Position, scene.Cameras[0].Direction, scene.Cameras[0].Up, Width, Height) { Fov = scene.Cameras[0].Fov };
                }
            });


            frameParsers.Add(typeof(FrameLightsource), item =>
            {
                if (lights == null)
                {
                    lights = new List<ILight>();
                }
                var lightsource = (FrameLightsource)item;
                switch (lightsource.LightType)
                {
                    case LightSourceTypes.Point:
                        lights.Add(new PointLight(lightsource));
                        break;
                    case LightSourceTypes.EnvironmentMap:
                        lights.Add(envMap = new InfiniteLight(lightsource));
                        break;
                    case LightSourceTypes.Area:
                        //if (AreaLightAsMeshLight)
                        lights.Add(new MeshLight(lightsource));
                        break;
                }
            });
        }

        #region IRenderConfig Members

        public string SavePath
        {
            get;
            set;
        }

        public string FrameName
        {
            get;
            set;
        }

        public int Width
        {
            get;
            set;
        }

        public int Height
        {
            get;
            set;
        }
        public void RunCommand(int index)
        { }
        public void SaveImage(string fn)
        {
            throw new NotImplementedException();
        }

        public void SaveRawImage(string fn)
        {
            throw new NotImplementedException();
        }


        public void OpenFrame(FrameDescription frame)
        {
            this.config = frame;

            this.FrameName = frame.FrameName;
            Width = frame.Get<int>("ImageWidth");
            Height = frame.Get<int>("ImageHeight");

            Directory.SetCurrentDirectory(frame.WorkingDir);
            foreach (var frameElement in frame.Elements.Where(frameElement => frameParsers.ContainsKey(frameElement.GetType())))
            {
                frameParsers[frameElement.GetType()](frameElement);
            }

            primitives = new List<GLScenePrimitive>();

            bboxes = new List<AABB>();
            var helper = new BoundingVolumeHierarchyAccelerationHelper();
            var s = new RayEngineScene();
               //(scene, null);
            helper.Init( s, s.Meshes.ToArray());
            
            bboxes.AddRange(helper.nodes.Select(item=>item.Bounds));
        }

        public void Start()
        {
            
        }

        public void Interrupt()
        {
            
        }

        public void ConsumeJob(IRenderJob job, Action<IRenderJobResult> resultCallback)
        {
            throw new NotImplementedException();
        }

        public string GetStats()
        {
            return "GL RENDERER";
        }

        public void Dispose()
        {
            
        }

        public IImagePlaneComponent GetImagePlane()
        {
            return null;
        }

        #endregion

        private GLScenePrimitive prim;
        public void SetupOpenGL()
        {
            ICamera cameraInfo = this.Camera;
            vertexShaderHandle = GL.CreateShader(ShaderType.VertexShader);
            fragmentShaderHandle = GL.CreateShader(ShaderType.FragmentShader);

            GL.ShaderSource(vertexShaderHandle, vertexShaderSource);
            GL.ShaderSource(fragmentShaderHandle, fragmentShaderSource);

            GL.CompileShader(vertexShaderHandle);
            GL.CompileShader(fragmentShaderHandle);

            Console.WriteLine(GL.GetShaderInfoLog(vertexShaderHandle));
            Console.WriteLine(GL.GetShaderInfoLog(fragmentShaderHandle));

            // Create program
            shaderProgramHandle = GL.CreateProgram();

            GL.AttachShader(shaderProgramHandle, vertexShaderHandle);
            GL.AttachShader(shaderProgramHandle, fragmentShaderHandle);

            GL.LinkProgram(shaderProgramHandle);

            Console.WriteLine(GL.GetProgramInfoLog(shaderProgramHandle));


            // Set uniforms
            projectionMatrixLocation = GL.GetUniformLocation(shaderProgramHandle, "projection_matrix");
            modelviewMatrixLocation = GL.GetUniformLocation(shaderProgramHandle, "modelview_matrix");

            float aspectRatio = Width / (float)(Height);
            Matrix4.CreatePerspectiveFieldOfView((float)Math.PI / 2, aspectRatio, 1, 1000, out projectionMatrix);
            modelviewMatrix = Matrix4.LookAt(new Vector3(Camera.Position.X, Camera.Position.Y, Camera.Position.Z), new Vector3(Camera.Target.X, Camera.Target.Y, Camera.Target.Z), new Vector3(Camera.Up.X, Camera.Up.Y,Camera.Up.Z));

            GL.UniformMatrix4(projectionMatrixLocation, false, ref projectionMatrix);
            GL.UniformMatrix4(modelviewMatrixLocation, false, ref modelviewMatrix);
            prim = new GLScenePrimitive();
            GL.GenVertexArrays(1, out prim.vaoHandle);
            GL.BindVertexArray(prim.vaoHandle);
            GL.GenBuffers(1, out prim.positionVboHandle);
            GL.BindBuffer(BufferTarget.ArrayBuffer, prim.positionVboHandle);

            GL.BufferData(BufferTarget.ArrayBuffer,
                   new IntPtr(scene.Vertices.Length * Vector3.SizeInBytes),
                   scene.Vertices, BufferUsageHint.StaticDraw);

            foreach (var geometryInfo in scene.Geometry)
            {

                prim = new GLScenePrimitive() { geoData = geometryInfo };
                primitives.Add(prim);
                //GL.GenVertexArrays(1, out prim.vaoHandle);
                //GL.BindVertexArray(prim.vaoHandle);

                //GL.GenBuffers(1, out prim.positionVboHandle);
                //GL.BindBuffer(BufferTarget.ArrayBuffer, prim.positionVboHandle);
                //var positionVboData = geometryInfo.VertexData.TodVectorList().ToArray();
                //GL.BufferData<Vector>(BufferTarget.ArrayBuffer,
                //    new IntPtr(positionVboData.Length * Vector3.SizeInBytes),
                //    positionVboData, BufferUsageHint.StaticDraw);

                //GL.GenBuffers(1, out prim.normalVboHandle);
                //GL.BindBuffer(BufferTarget.ArrayBuffer, prim.normalVboHandle);
                //GL.BufferData<Vector>(BufferTarget.ArrayBuffer,
                //    new IntPtr(positionVboData.Length * Vector3.SizeInBytes),
                //    positionVboData, BufferUsageHint.StaticDraw);

               
                
            }
            GL.EnableVertexAttribArray(0);
            //GL.EnableVertexAttribArray(1);

            GL.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, true, Vector3.SizeInBytes, 0);
            //GL.VertexAttribPointer(1, 3, VertexAttribPointerType.Float, true, Vector3.SizeInBytes, 0);
            //primitives.AddRange(scene.Geometry.Except(new[]{scene.Geometry.First()}).Select(item => new GLScenePrimitive() { geoData = item }));
            GL.BindAttribLocation(shaderProgramHandle, 0, "in_position");
            //GL.BindAttribLocation(shaderProgramHandle, 1, "in_normal");

            // Other state
            GL.Enable(EnableCap.DepthTest);
            GL.ClearColor(System.Drawing.Color.MidnightBlue);
        }


        public void SetupView()
        {
            GL.Viewport(0, 0, Width, Height);

            GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
            projectionMatrixLocation = GL.GetUniformLocation(shaderProgramHandle, "projection_matrix");
            modelviewMatrixLocation = GL.GetUniformLocation(shaderProgramHandle, "modelview_matrix");

            float aspectRatio = Width / (float)(Height);
            Matrix4.CreatePerspectiveFieldOfView((float)Math.PI / 2, aspectRatio, 1, 1000, out projectionMatrix);
            modelviewMatrix = Matrix4.LookAt(new Vector3(Camera.Position.X, Camera.Position.Y, Camera.Position.Z), new Vector3(Camera.Target.X, Camera.Target.Y, Camera.Target.Z), new Vector3(Camera.Up.X, Camera.Up.Y, Camera.Up.Z));

            GL.UniformMatrix4(projectionMatrixLocation, false, ref projectionMatrix);
            GL.UniformMatrix4(modelviewMatrixLocation, false, ref modelviewMatrix);
        }

        public void StepForward()
        {
            Camera.Position += Camera.Target*0.25f;
        }

        public void StepBackward()
        {
            //Camera.Position -= (Point)(Camera.Target * 0.25f);
        }

        internal void RenderFrame()
        {
            GL.Viewport(0, 0, Width, Height);

            GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
            
            GL.BindBuffer(BufferTarget.ArrayBuffer, prim.positionVboHandle);
            GL.UseProgram(shaderProgramHandle);

            foreach (var glScenePrimitive in primitives)
            {
                //GL.BindBuffer(BufferTarget.ArrayBuffer, glScenePrimitive.positionVboHandle);
                var indicesVboData = glScenePrimitive.geoData.IndexData.Select(item => (uint)(item)).ToArray();
                GL.DrawElements(PrimitiveRenderMode, indicesVboData.Length, DrawElementsType.UnsignedInt, indicesVboData);
            }
            

            foreach (var bbox in bboxes)
            {
                RenderBox(bbox);
            }
            

            //SwapBuffers();
        }

        private void RenderBox(AABB bbox)
        {
            GL.Begin(BeginMode.LineStrip);
            GL.Color3(1f, 0, 0);
            GL.Vertex3(bbox.Min.ToArray());
            GL.Vertex3(bbox.Min.x, bbox.Max.y, bbox.Min.z);
            GL.Vertex3(bbox.Min.x, bbox.Min.y, bbox.Max.z);
            GL.Vertex3(bbox.Min.x, bbox.Max.y, bbox.Max.z);
            GL.Vertex3(bbox.Max.ToArray());
            GL.Vertex3(bbox.Max.x, bbox.Min.y, bbox.Min.z);
            GL.Vertex3(bbox.Max.x, bbox.Max.y, bbox.Min.z);
            GL.Vertex3(bbox.Max.x, bbox.Min.y, bbox.Max.z);
            GL.End();
        }
    }
}
